<?php
/**
 * 客户端操作类, 将原版单个DHT分为多个DHT同时进行, 加快采集速度
 */
class Client{
    /**
     * swoole_server对象
     * @var swoole_server
     */
    private $serv;
    /**
     * 路由表
     * @var array
     */
    private $table = [];
    /**
     * node id
     * @var string
     */
    private $nid;
    /**
     * 进程列表
     * @var array
     */
    private $worker = [];
    /**
     * 长期在线节点列表
     * @var array
     */
    private $bootstrap_nodes = array(
        array('router.bittorrent.com', 6881),
        array('dht.transmissionbt.com', 6881),
        array('router.utorrent.com', 6881)
    );
    /**
     * 定义一个成员属性, 保存开启时间
     * @var integer
     */
    private $start;
    /**
     * 保存端口, 为了后面的统计
     * @var integer
     */
    private $port;

    /**
     * 构造函数,初始化客户端
     * @param integer $port 要绑定的端口号
     * @return void
     */
    public function __construct($port){
        $this->port = $port;
        $this->start = time();
        // 先为客户端生成一个node id
        $this->nid = Base::get_node_id();
        // 初始化swoole_server对象, 绑定指定端口, 使用UDP协议
        $this->serv = new swoole_server('0.0.0.0', $port, SWOOLE_PROCESS, SWOOLE_SOCK_UDP);
        // 配置swoole_server对象
        $this->serv->set(array(
            // 设置主进程数量, 一般为CPU的1至4倍
            'worker_num' => WORKER_NUM,
            // 设置为守护模式
            'daemonize' => false,
            // 设置最大任务数
            'max_request' => MAX_REQUEST,
            // 设置数据包分发策略
            'dispatch_mode' => 3,
            // 定义错误信息保存文件路径
            'log_file' => ABSPATH . 'error.log'
        ));
        // 定义onWorkerStart进程启动回调函数
        $this->serv->on('WorkerStart', function($serv, $worker_id){
            // 添加一个定时器
            $serv->addtimer(AUTO_FIND_TIME);
            // 进程启动后先进行一次find_node自动查找
            $this->auto_find_node();
        });
        // 定义onReceive数据接收回调函数
        $this->serv->on('Receive', function($serv, $fd, $from_id, $data){
            // 检查数据长度
            if(strlen($data) == 0)
                return false;

            // 对数据进行解码
            $msg = Base::decode($data);

            // 检查是否为需要的数据
            if(!isset($msg['y']))
                return false;

            // 获取对端链接信息, udp链接需要加上$from_id参数
            $fdinfo = $serv->connection_info($fd, $from_id);

            // 对接收到的数据进行类型判断
            if($msg['y'] == 'r'){
                // 如果是回复, 且包含nodes信息
                if(array_key_exists('nodes', $msg['r']))
                    // 对nodes进行操作
                    $this->response_action($msg, array($fdinfo['remote_ip'], $fdinfo['remote_port']));
            }elseif($msg['y'] == 'q'){
                // 如果是请求, 则执行请求判断
                $this->request_action($msg, array($fdinfo['remote_ip'], $fdinfo['remote_port']));
            }else{
                // 都不是则直接略过
                return false;
            }
        });
        // 定义onTimer定时器回调函数
        $this->serv->on('Timer', function($interval){
            // 清空进程列表
            $this->worker = [];
            // 循环创建操作进程
            for($i=0; $i<MAX_PROCESS; $i++){
                // 定义进程方法, 进行find_node自动查找
                $process = new swoole_process(function(){
                    $this->auto_find_node();
                });
                // 启动进程
                $pid = $process->start();
                // 将进程加入进程列表
                $this->worker[$pid] = $process;
                // 结束进程操作
                swoole_process::wait();
            }
        });
    }

    /**
     * 启动方法
     * @return void
     */
    public function run(){
        // 启动swoole_server进程
        $this->serv->start();
    }

    /**
     * find_node自动查找方法
     * @return void
     */
    private function auto_find_node(){
        // 如果路由表中没有数据则先加入DHT网络
        if(count($this->table) == 0)
            return $this->join_dht();

        // 循环处理路由表
        while(count($this->table)){
            // 从路由表中删除第一个node并返回被删除的node
            $node = array_shift($this->table);
            // 发送查找find_node到node中
            $this->find_node(array($node->ip, $node->port), $node->nid);
            // 加一个发送间隔, 避免数据发送过快引起错误
            sleep(NEXT_FIND_NODE_TIME);
        }
    }

    /**
     * 加入DHT网络
     * @return void
     */
    private function join_dht(){
        // 循环操作
        foreach($this->bootstrap_nodes as $node){
            // 将node域名解析为IP地址, 并发送find_node请求
            $this->find_node(array(gethostbyname($node[0]), $node[1]));
        }
    }

    /**
     * 发送find_node请求
     * @param  array $address 对端链接信息
     * @param  string $id      传入node id, 以便伪造相邻node
     * @return void
     */
    private function find_node($address, $id = null){
        // 若未指定id则使用自身node id
        if(is_null($id))
            $mid = $this->nid;
        else
            // 否则伪造一个相邻id
            $mid = Base::get_neighbor($id, $this->nid);

        // 定义发送数据
        $msg = array(
            't' => Base::entropy(2),
            'y' => 'q',
            'q' => 'find_node',
            'a' => array(
                'id' => $this->nid,
                'target' => $mid
            )
        );
        // 发送请求数据到对端
        $this->send_response($msg, $address);
    }

    /**
     * 处理接收到的请求
     * @param  array $msg     接收到的请求数据
     * @param  array $address 对端链接信息
     * @return void
     */
    private function request_action($msg, $address){
        // 对请求数据进行判断
        switch($msg['q']){
            // 处理get_peers请求
            case 'get_peers':
                $this->on_get_peers($msg, $address);
                break;
            // 处理announce_peer请求
            case 'announce_peer':
                $this->on_announce_peer($msg, $address);
                break;
            // 若都不是, 发送错误信息
            default:
                // 生成服务器错误信息
                $msg = array(
                    't' => $msg['t'],
                    'y' => 'e',
                    'e' => array(
                        202,
                        'Server Error'
                    )
                );
                // 发送错误信息到对端
                return $this->send_response($msg, $address);
        }
    }

    /**
     * 处理接收到的find_node回复数据
     * @param  array $msg     接收到的find_node回复
     * @param  array $address 对端链接信息
     * @return void
     */
    private function response_action($msg, $address){
        // 先检查接收到的信息是否正确
        if(!isset($msg['r']['nodes']) || !isset($msg['r']['nodes'][1]))
            return false;

        // 对nodes数据进行解码
        $nodes = Base::decode_nodes($msg['r']['nodes']);

        // 对nodes循环处理
        foreach($nodes as $node){
            // 将node加入到路由表中
            $this->append($node);
        }
    }

    /**
     * 处理get_peers请求数据
     * @param  array $msg     接收到的get_peers请求数据
     * @param  array $address 对端链接信息
     * @return void
     */
    private function on_get_peers($msg, $address){
        // 获取info_hash信息
        $infohash = $msg['a']['info_hash'];
        // 获取node id
        $id = $msg['a']['id'];

        $h = strtoupper(bin2hex($infohash));
        if(strlen($h)==40)
            $this->write($h);

        // 生成回复数据
        $msg = array(
            't' => $msg['t'],
            'y' => 'r',
            'r' => array(
                'id' => $this->nid,
                // 随机获取一批node列表返回
                'nodes' => Base::encode_nodes($this->get_nodes(16)),
                'token' => substr($infohash, 0, 2)
            )
        );
        // 将node加入路由表
        $this->append(new Node($id, $address[0], $address[1]));
        // 向对端发送回复数据
        $this->send_response($msg, $address);
    }

    /**
     * 处理announce_peer请求数据
     * @param  array $msg     接收到的announce_peer请求数据
     * @param  array $address 对端链接信息
     * @return void
     */
    private function on_announce_peer($msg, $address){
        // 获取infohash
        $infohash = $msg['a']['info_hash'];
        // 获取token
        $token = $msg['a']['token'];
        // 获取node id
        $id = $msg['a']['id'];
        $h = strtoupper(bin2hex($infohash));
        if(strlen($h)==40)
            $this->write($h);

        // 验证token是否正确
        /*if(substr($infohash, 0, 2) == $token){
            // 将info_hash转换为大写十六进制字符串
            $h = strtoupper(bin2hex($infohash));
            $this->write($h);
        }*/

        // 生成回复数据
        $msg = array(
            't' => $msg['t'],
            'y' => 'r',
            'r' => array(
                'id' => $this->nid
            )
        );
        // 发送请求回复
        $this->send_response($msg, $address);
    }

    /**
     * 发送数据到对端
     * @param  array $msg     要发送的数据
     * @param  array $address 对端链接信息
     * @return void
     */
    private function send_response($msg, $address){
        // 将数据进行编码处理并发送到对端
        $this->serv->sendto($address[0], $address[1], Base::encode($msg));
    }

    /**
     * 将node添加到路由表
     * @param  Node $node node模型
     * @return void
     */
    private function append($node){
        // 检查node id是否正确
        if(!isset($node->nid[19]))
            return false;

        // 检查是否为自身node id
        if($node->nid == $this->nid)
            return false;

        // 检查是否为自身IP
        if($node->ip == LOCAL_IP)
            return false;

        // 检查node是否已存在
        if(in_array($node, $this->table))
            return false;

        // 如果路由表中的项达到100时, 删除第一项
        if(count($this->table) >= 300)
            array_shift($this->table);

        // 将node加入路由表
        return array_push($this->table, $node);
    }

    /**
     * 随机获取指定数量的node列表
     * @param  integer $len 要获取的数量
     * @return array       获取到的node列表
     */
    private function get_nodes($len = 8){
        // 检查路由表是否已达到要获取的数据
        if(count($this->table) <= $len)
            // 未达到则直接返回路由表
            return $this->table;

        // 定义一个数组, 保存获取到的node列表
        $nodes = array();

        // 循环获取node
        for($i=0; $i<$len; $i++){
            // 通过随机数获取node, 并加入到列表中
            $nodes[] = $this->table[mt_rand(0, count($this->table) - 1)];
        }

        // 返回获取到的node列表
        return $nodes;
    }

    private function write($hash){
        $fp = fopen(ABSPATH . '/infohash.log', 'w+');
        // 为文件加锁
        flock($fp, LOCK_EX);
        // 将hash信息以json方式写入文件
        fwrite($fp, $hash."\n");
        // 解锁文件
        flock($fp, LOCK_UN);
        // 关闭文件
        fclose($fp);
    }

    /**
     * 写入info_hash信息
     * @return void
     */
    /*private function write(){
        // 定义一个数组, 以便保存hash
        $json = array();

        // 循环获取hash信息
        foreach($this->serv->table as $info){
            $json[$info['infohash']] = $info['hot'];
        }
        var_dump($this->serv->table);
        var_dump($json);

        // 创建并打开infohash.log文件
        $fp = fopen(ABSPATH . '/infohash.log', 'w');
        if(!$fp){
            echo "Can't open " . ABSPATH . '/infohash.log';
        }
        // 为文件加锁
        flock($fp, LOCK_EX);
        // 将hash信息以json方式写入文件
        fwrite($fp, json_encode($json));
        // 解锁文件
        flock($fp, LOCK_UN);
        // 关闭文件
        fclose($fp);

        // 计算运行时间
        $now = time() - $this->start;
        $run = '';

        if($now > 0){
            $day = floor($now / 86400);
            $hour = floor(($now % 86400) / 3600);
            $minute = floor(($now % 3600) / 60);
            $second = floor($now % 60);

            $run = $day . ' day, ' . $hour . ' hour, ' . $minute . ' minute, ' . $second . ' second';
        }else{
            $run = '0 day, 0 hour, 0 minute, 0 second';
        }

        $txt = date('Y-m-d H:i:s', $this->start) . " - DHT Start, listen: {$this->port}...\n";
        $txt .= "====================================================================================\n\n";
        $txt .= "                        run time:                " . $run . "\n                        collect node:            " . count($this->table) . "\n                        info_hash count:         " . $this->serv->table->count() . "\n\n";
        $txt .= "====================================================================================\n\n";

        $ccc = '';

        foreach($this->serv->table as $key => $val){
            $ccc .= "info_hash: " . $val['infohash'] . " | hot: " . $val['hot'] . "\n";
        }

        file_put_contents(ABSPATH . '/' . $this->port . '.state', $txt."\n\n\n\n".$ccc);
    }*/
}